/*
 * Copyright (C) 2015 Square, Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package okhttp3.internal.io;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.IOException;
import okio.Okio;
import okio.Sink;
import okio.Source;

/**
 * Access to read and write files on a hierarchical data store. Most callers
 * should use the {@link #SYSTEM} implementation, which uses the host machine's
 * local file system. Alternate implementations may be used to inject faults
 * (for testing) or to transform stored data (to add encryption, for example).
 * 
 * <p>
 * All operations on a file system are racy. For example, guarding a call to
 * {@link #source} with {@link #exists} does not guarantee that
 * {@link FileNotFoundException} will not be thrown. The file may be moved
 * between the two calls!
 * 
 * <p>
 * This interface is less ambitious than {@link java.nio.file.FileSystem}
 * introduced in Java 7. It lacks important features like file watching,
 * metadata, permissions, and disk space information. In exchange for these
 * limitations, this interface is easier to implement and works on all versions
 * of Java and Android.
 */
public interface FileSystem {
	/** The host machine's local file system. */
	FileSystem SYSTEM = new FileSystem() {
		@Override
		public Source source(File file) throws FileNotFoundException {
			return Okio.source(file);
		}

		@Override
		public Sink sink(File file) throws FileNotFoundException {
			try {
				return Okio.sink(file);
			} catch (FileNotFoundException e) {
				// Maybe the parent directory doesn't exist? Try creating it
				// first.
				file.getParentFile().mkdirs();
				return Okio.sink(file);
			}
		}

		@Override
		public Sink appendingSink(File file) throws FileNotFoundException {
			try {
				return Okio.appendingSink(file);
			} catch (FileNotFoundException e) {
				// Maybe the parent directory doesn't exist? Try creating it
				// first.
				file.getParentFile().mkdirs();
				return Okio.appendingSink(file);
			}
		}

		@Override
		public void delete(File file) throws IOException {
			// If delete() fails, make sure it's because the file didn't exist!
			if (!file.delete() && file.exists()) {
				throw new IOException("failed to delete " + file);
			}
		}

		@Override
		public boolean exists(File file) {
			return file.exists();
		}

		@Override
		public long size(File file) {
			return file.length();
		}

		@Override
		public void rename(File from, File to) throws IOException {
			delete(to);
			if (!from.renameTo(to)) {
				throw new IOException("failed to rename " + from + " to " + to);
			}
		}

		@Override
		public void deleteContents(File directory) throws IOException {
			File[] files = directory.listFiles();
			if (files == null) {
				throw new IOException("not a readable directory: " + directory);
			}
			for (File file : files) {
				if (file.isDirectory()) {
					deleteContents(file);
				}
				if (!file.delete()) {
					throw new IOException("failed to delete " + file);
				}
			}
		}
	};

	/** Reads from {@code file}. */
	Source source(File file) throws FileNotFoundException;

	/**
	 * Writes to {@code file}, discarding any data already present. Creates
	 * parent directories if necessary.
	 */
	Sink sink(File file) throws FileNotFoundException;

	/**
	 * Writes to {@code file}, appending if data is already present. Creates
	 * parent directories if necessary.
	 */
	Sink appendingSink(File file) throws FileNotFoundException;

	/**
	 * Deletes {@code file} if it exists. Throws if the file exists and cannot
	 * be deleted.
	 */
	void delete(File file) throws IOException;

	/** Returns true if {@code file} exists on the file system. */
	boolean exists(File file);

	/**
	 * Returns the number of bytes stored in {@code file}, or 0 if it does not
	 * exist.
	 */
	long size(File file);

	/**
	 * Renames {@code from} to {@code to}. Throws if the file cannot be renamed.
	 */
	void rename(File from, File to) throws IOException;

	/**
	 * Recursively delete the contents of {@code directory}. Throws an
	 * IOException if any file could not be deleted, or if {@code dir} is not a
	 * readable directory.
	 */
	void deleteContents(File directory) throws IOException;
}
